Skip to content

feat: add musl target support#273

Merged
branchseer merged 34 commits intomainfrom
claude/fix-vite-musl-support-mPUNe
Mar 20, 2026
Merged

feat: add musl target support#273
branchseer merged 34 commits intomainfrom
claude/fix-vite-musl-support-mPUNe

Conversation

@branchseer
Copy link
Member

@branchseer branchseer commented Mar 19, 2026

Summary

  • Always use seccomp in fspy to infer inputs
  • Add a dedicated test-musl CI job that runs the full test suite against x86_64-unknown-linux-musl

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv

@branchseer branchseer marked this pull request as draft March 19, 2026 06:32
@branchseer branchseer marked this pull request as ready for review March 20, 2026 02:18
@branchseer branchseer marked this pull request as draft March 20, 2026 02:18
@branchseer branchseer marked this pull request as ready for review March 20, 2026 02:30
@branchseer branchseer force-pushed the claude/fix-vite-musl-support-mPUNe branch 4 times, most recently from ee4af3d to 1d7f67f Compare March 20, 2026 03:16
claude added 18 commits March 20, 2026 11:48
fspy's LD_PRELOAD-based file tracking does not work with statically-linked
musl binaries. This disables fspy inference at execution time on musl targets
while keeping plan-level config consistent across all targets.

- Disable path_accesses tracking in execute_spawn when target_env is musl
- Add `requires_fspy` field to e2e test config to skip fspy-dependent tests
- Add dedicated musl test job (x86_64-unknown-linux-musl) in CI

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
On musl targets, LD_PRELOAD-based file tracking is not available because musl
does not support cdylib. Instead, always use seccomp+unotify for file access
tracking which works with all binary types.

- Exclude fspy_preload_unix from musl builds (cdylib not supported)
- Always use seccomp path in spawn/linux on musl (skip ELF/LD_PRELOAD check)
- Add dedicated test-musl CI job (x86_64-unknown-linux-musl)
- Temporarily disable other CI jobs for faster iteration

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
On musl targets, LD_PRELOAD-based file tracking is not available because musl
does not support cdylib. Instead, always use seccomp+unotify for file access
tracking which works with all binary types.

Changes:
- Exclude fspy_preload_unix from musl builds (cdylib not supported)
- Remove preload_path field from Payload on musl
- Always use seccomp path in spawn/linux on musl (skip ELF/LD_PRELOAD check)
- Fix ioctl request type mismatch (c_ulong vs Ioctl) for musl compatibility
- Add statx, access, faccessat, faccessat2 syscall handlers to seccomp filter
  for complete file access tracking without LD_PRELOAD
- Add dedicated test-musl CI job (x86_64-unknown-linux-musl)

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
Use an Alpine Linux container for the musl test job so tests run with
musl as the native libc. This avoids cross-compilation issues with
static musl binaries where ctor's .init_array entries are dropped.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
The .cargo/config.toml sets rustflags with a zig linker wrapper for musl
targets. Override via env var to use the system cc in Alpine.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
Use sed to strip musl target linker config from .cargo/config.toml
since Alpine's system cc is already musl-based.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
On musl, the artifact module (preload library writing) and NativeStr
import are unused since LD_PRELOAD is not available.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
The ctor crate's #[ctor] inside macro expansions doesn't work on musl
targets because the .init_array entry is dropped by the linker.

Replace with a two-part approach:
- Use linkme distributed_slice to register subprocess handlers (works
  reliably on all targets since it uses custom linker sections)
- Use a crate-level subprocess_dispatch_ctor!() macro that each test
  crate calls at crate scope (not inside a function) for the #[ctor]
  dispatcher

Each crate that uses command_for_fn! must now also call
subprocess_dispatch_ctor!() at crate scope.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
- Add cargo-shear ignore for ctor and linkme in subprocess_test
- Use --security-opt seccomp=unconfined for Alpine container since
  fspy uses seccomp user notifications

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
ctor 0.6 generates .init_array entries that get dropped by the linker
on musl targets. ctor 0.2 uses a different code generation approach
that works reliably across all targets.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
The ctor issue on musl is fundamental — neither v0.2 nor v0.6 works
in Alpine containers. The .init_array entries are dropped regardless
of ctor version.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
On musl targets, `std::env::args()` returns empty during `.init_array`
constructors because the Rust runtime hasn't initialized its argument
storage yet. This caused `subprocess_dispatch()` and `init_impl()` to
silently skip subprocess dispatch, making all subprocess-based tests
fail (fspy, pty_terminal, fspy_shared IPC tests).

Fix by falling back to reading `/proc/self/cmdline` directly via raw
libc calls when `std::env::args()` is empty. The libc-level open/read
calls work during `.init_array` even when the Rust runtime isn't ready.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
claude and others added 16 commits March 20, 2026 11:48
The previous implementation filtered out empty strings from cmdline
args, but empty args are valid (e.g., `()` encodes to empty base64).
This caused subprocess dispatch to fail for tests using unit arg type
because the arg count dropped below 3.

Now only removes the trailing empty string from the final null
terminator instead of all empty strings.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
On musl/seccomp targets, `std::fs::read_dir()` opens the directory
with `O_DIRECTORY` but doesn't call `getdents64` until the iterator
is consumed. The seccomp handler only set READ_DIR on `getdents64`
notifications, so lazy `read_dir()` calls were tracked as READ
instead of READ_DIR.

Fix by detecting the `O_DIRECTORY` flag in the open/openat handler
and adding `READ_DIR` to the access mode. This matches the behavior
of the LD_PRELOAD interceptor on glibc targets.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
Root cause: vite_task explicitly disabled fspy on musl targets
(`!cfg!(target_env = "musl")`), not realizing that seccomp unotify
provides equivalent file access tracing to LD_PRELOAD. This caused
all cache invalidation to fail on musl since file accesses were
never recorded.

Additional fixes:
- Set CC env vars for musl cross-compilation so cc-rs can find the
  zig CC wrapper (fixes stackalloc build failure)
- Use openat(AT_FDCWD) instead of open() in seccomp arg_types test
  because musl's open() uses the native `open` syscall on x86_64,
  which isn't intercepted by the test's openat-only handler
- Refactor shm_io test to use subprocess_test infrastructure instead
  of raw #[ctor] (fixes musl args unavailability during .init_array)
- Add required-features to fspy_seccomp_unotify arg_types test so
  it only compiles when supervisor+target features are available

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
- Strip [env] section in CI musl job alongside [target.*musl] sections,
  since the CC_*_musl env vars point to the zigcc wrapper which isn't
  available on native Alpine (system gcc is used instead)
- Add ctor to cargo-shear ignored list in fspy_shared since it's used
  transitively through the subprocess_dispatch_ctor!() macro

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
Alpine's Node.js 22.15.1 package doesn't enable TypeScript type
stripping by default, causing ERR_UNKNOWN_FILE_EXTENSION errors
when running the .ts test tool scripts. Set NODE_OPTIONS to enable
--experimental-strip-types explicitly.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
The E2E test harness clears env vars and only passes PATH, NO_COLOR,
TERM. On Alpine musl CI, NODE_OPTIONS=--experimental-strip-types is
needed for .ts test tools. Inherit NODE_OPTIONS when present.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
Alpine's Node.js package (v22.15.1) is compiled without TypeScript
type-stripping support (ERR_NO_TYPESCRIPT). Install the official
Node.js musl binary from unofficial-builds.nodejs.org which includes
full TypeScript support needed by the test tool scripts (.ts files).

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
The 'latest-v22.x' URL pattern doesn't exist on unofficial-builds.
Query the index.json to find the latest v22 version and use the
specific version URL to download the musl binary.

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
- Remove speculative seccomp comment from CI workflow
- Use .node-version file for Node.js version in Alpine CI
- Remove [env] CC wrapper vars from .cargo/config.toml and CI sed
- Revert O_DIRECTORY check in seccomp handler; instead consume a dir
  entry in the rust_std test so getdents64 fires
- Fix misleading "musl does not support cdylib" comment; update fspy
  README with musl section
- Revert to plain #[ctor::ctor] in command_for_fn! macro; remove
  linkme distributed slice infrastructure and subprocess_dispatch_ctor
- Keep /proc/self/cmdline fallback for musl arg reading in init_impl
- Remove all requires_fspy from snapshot toml files and Rust code

https://claude.ai/code/session_01Cqj3gbQjb7yFe49f1tfwYv
The seccomp override is no longer needed since fspy seccomp tracing
was fixed for musl in a prior commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ker scripts

- Use node:22-alpine3.21 image instead of alpine:3.21 to get Node.js
  with TypeScript support pre-installed
- Use corepack to install pnpm, respecting packageManager in package.json
- Update .cargo linker scripts to detect Linux hosts and use system cc
  directly, removing the need for the sed workaround in CI
- Split Rust setup into rustup install + toolchain install from
  rust-toolchain.toml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

The linker path specified via rustflags (-C linker=...) is passed to
rustc which doesn't resolve it relative to the config file. Using
Cargo's native `linker` key resolves relative paths correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nv override

- Replace raw libc::open/read/close with safe nix equivalents in
  read_proc_cmdline
- Remove CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS env var from
  Alpine CI — the linker scripts detect Linux and use system cc directly
- Revert linker config to rustflags (needed for cargo-zigbuild bindeps)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@branchseer branchseer force-pushed the claude/fix-vite-musl-support-mPUNe branch from 1d7f67f to 29bfaa0 Compare March 20, 2026 03:48
@branchseer branchseer merged commit f385930 into main Mar 20, 2026
16 of 18 checks passed
@branchseer branchseer deleted the claude/fix-vite-musl-support-mPUNe branch March 20, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants